/* *
 * --ライセンスについて--
 *
 * 「本ファイルの内容は Mozilla Public License Version 1.1 (「本ライセンス」)
 * の適用を受けます。
 * 本ライセンスに従わない限り本ファイルを使用することはできません。
 * 本ライセンスのコピーは http://www.mozilla.org/MPL/ から入手できます。
 *
 * 本ライセンスに基づき配布されるソフトウェアは、「現状のまま」で配布されるものであり、
 * 明示的か黙示的かを問わず、いかなる種類の保証も行われません。
 * 本ライセンス上の権利および制限を定める具体的な文言は、本ライセンスを参照してください。
 *
 * オリジナルコードおよび初期開発者は、N_H (h.10x64@gmail.com) です。
 *
 * N_H によって作成された部分の著作権表示は次のとおりです。
 *
 * Copyright (C)N_H 2012
 *
 * このファイルの内容は、上記に代えて、
 * GNU General License version2 以降 (以下 GPL とする)、
 * GNU Lesser General Public License Version 2.1 以降 (以下 LGPL とする)、
 * の条件に従って使用することも可能です。
 * この場合、このファイルの使用には上記の条項ではなく GPL または LGPL の条項が適用されます。
 * このファイルの他者による使用を GPL または LGPL の条件によってのみ許可し、
 * MPL による使用を許可したくない対象者は、上記の条項を削除することでその意思を示し、
 * 上記条項を GPL または LGPL で義務付けられている告知およびその他の条項に置き換えてください。
 * 対象者が上記の条項を削除しない場合、
 * 受領者は MPL または GPL または LGPL ライセンスのいずれによってもこのファイルを
 * 使用することができます。」
 *
 * -- License --
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License。You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND、either express or implied。See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Initial Developer of the Original Code is
 *   N_H (h.10x64@gmail.com).
 *
 * Portions created by the Initial Developer are Copyright (C)N_H 2012
 * the Initial Developer。All Rights Reserved.
 *
 * Alternatively、the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL")、or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above。If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL、and not to allow others to
 * use your version of this file under the terms of the MPL、indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL。If you do not delete
 * the provisions above、a recipient may use your version of this file under
 * the terms of any one of the MPL、the GPL or the LGPL.
 *
 * */
package com.magiciansforest.audio.soundrenderer.logic.sound;

import com.jme3.audio.Listener;
import com.jme3.math.Vector3f;
import com.magiciansforest.audio.data.Air;
import com.magiciansforest.audio.file.hrtf.HRTF;
import com.magiciansforest.audio.file.wav.FormatChunk;
import com.magiciansforest.audio.soundrenderer.ApplicationBase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;

/**
 *
 * @author N_H
 */
public class AudioRenderThread extends Thread {

    public static final int RENDER_PER_SECOND = 30;
    private static AudioRenderThread singleton;
    private double unitSize = 1.0;
    private int renderSpan;
    private AudioFormat outputFormat;
    private HRTF hrtf;
    private Air air;
    private SourceDataLine sdl = null;
    private ApplicationBase app;
    private boolean isInterrupted = false;
    private long startTime = 0;
    private double now = 0, renderedTime = 0;
    private Listener listener;
    private Vector3f listenerPos;
    private Vector3f lastListenerPos;
    //AudioNodes
    private final List<SoundSource> sourceDataList = new ArrayList<SoundSource>();
    //Output
    private AudioOutputThread out;
    private boolean doesRendering = false, fileReady = false;
    private boolean listEdit = false, useList = false;

    private AudioRenderThread(HRTF hrtf, Air air, AudioFormat outputFormat) throws LineUnavailableException {
        this.outputFormat = outputFormat;
        this.hrtf = hrtf;
        this.air = air;
        this.renderSpan = Math.round((float) outputFormat.getSampleRate() / RENDER_PER_SECOND);
        setAudioFormat(outputFormat);
    }

    public static AudioRenderThread createAudioRenderThread(HRTF hrtf, Air air, AudioFormat outputFormat) throws LineUnavailableException {
        singleton = new AudioRenderThread(hrtf, air, outputFormat);
        return singleton;
    }

    public static AudioRenderThread getAudioRenderThread() {
        return singleton;
    }

    public void setApplication(ApplicationBase app) {
        this.app = app;
    }

    /*
     * Abandon
     */
    public void reset() {
        now = 0;
        renderedTime = 0;
        sourceDataList.clear();
    }

    /*
     * implement, override
     */
    
    @Override
    public void interrupt() {
        super.interrupt();
        if (out != null && out.isAlive()) {
            out.interrupt();
        }
        isInterrupted = true;
    }

    @Override
    public void run() {
        startTime = System.currentTimeMillis();
        while (!isInterrupted) {
            if ((doesRendering && !fileReady) || app.isPaused()) {
                yield();
            } else {
                double progressTime = (double) (System.currentTimeMillis() - startTime) / 1000;
                if (!listEdit && progressTime > renderedTime) { //@see TimeKeeper.java line 71
                    //Render
                    double[] mixBuf = new double[outputFormat.getChannels() * renderSpan];
                    double[] stereoMixBuf = new double[6 * renderSpan];

                    useList = true;
                    for (SoundSource src : sourceDataList) {
                        if (src.hasWavData() && src.getStatus() == SoundSource.PLAYING) {
                            double[] renderBuf = new double[renderSpan];
                            int readLength = src.read(renderBuf);
                            if (readLength > 0) {
                                double[] rendered = src.getBinauralFilter().filter(renderBuf);
                                for (int i = 0; i < renderSpan * outputFormat.getChannels(); i++) {
                                    mixBuf[i] += rendered[i];
                                }
                            }
                        }
                    }
                    useList = false;

                    byte[] spkBuf = new byte[mixBuf.length * outputFormat.getSampleSizeInBits() / 8];
                    byte[] outBuf = new byte[spkBuf.length];

                    toBytes(spkBuf, mixBuf, outputFormat.getSampleSizeInBits(), outputFormat.isBigEndian());
                    System.arraycopy(spkBuf, 0, outBuf, 0, spkBuf.length);

                    sdl.write(spkBuf, 0, spkBuf.length);
                    if (doesRendering && fileReady) {
                        out.add(outBuf);
                    }
                    renderedTime += (double) renderSpan / outputFormat.getSampleRate();
                }

                yield();
            }
        }

        if (sdl.isRunning()) {
            sdl.stop();
        }
        if (sdl.isOpen()) {
            sdl.drain();
            sdl.stop();
        }
    }

    /*
     * Method
     */
    public void resetTime() {
        renderedTime = 0;
        startTime = System.currentTimeMillis();
    }

    public void addSoundSource(SoundSource src) {
        while (useList) {
            Thread.yield();
        }
        listEdit = true;
        sourceDataList.add(src);
        listEdit = false;
    }

    public void removeSoundSource(SoundSource src) {
        while (useList) {
            Thread.yield();
        }
        listEdit = true;
        sourceDataList.remove(src);
        listEdit = false;
    }

    public AudioFormat getOutputFormat() {
        return outputFormat;
    }

    public void setListener(Listener listener) {
        this.listener = listener;
        lastListenerPos = listener.getLocation();
    }

    public void update(float tpf) {
        if (!doesRendering || (doesRendering && fileReady)) {
            if (!listEdit) {
                useList = true;
                lastListenerPos = (listenerPos == null) ? listener.getLocation() : listenerPos;
                listenerPos = listener.getLocation().clone();
                for (SoundSource src : sourceDataList) {
                    src.update(listenerPos, listener.getRotation(), tpf);
                }
                useList = false;
            }
        }
    }

    public void setUnitSize(double unitSize) {
        this.unitSize = unitSize;
    }

    public double getUnitSize() {
        return unitSize;
    }

    public double getRenderedTime() {
        return renderedTime;
    }

    public HRTF getHRTF() {
        return hrtf;
    }

    public Air getAir() {
        return air;
    }

    Vector3f getLastListenerPos() {
        return lastListenerPos;
    }

    public void setAudioFormat(AudioFormat format) throws LineUnavailableException {
        if (!doesRendering) {
            if (sdl != null) {
                if (sdl.isRunning()) {
                    sdl.stop();
                }
                if (sdl.isOpen()) {
                    sdl.drain();
                    sdl.close();
                }
            }

            sdl = AudioSystem.getSourceDataLine(format);
            sdl.open();
            sdl.start();

            this.outputFormat = format;
        }
    }

    public void setOutputFilePath(String path) throws IOException {
        if (out != null) {
            out.interrupt();
        }

        FormatChunk fmt =  new FormatChunk(
                1,
                outputFormat.getChannels(),
                Math.round(outputFormat.getFrameRate()),
                Math.round(outputFormat.getFrameRate()) * outputFormat.getChannels() * outputFormat.getSampleSizeInBits() / 8,
                outputFormat.getChannels() * outputFormat.getSampleSizeInBits() / 8, outputFormat.getSampleSizeInBits());
        out = new AudioOutputThread(path, fmt);

        fileReady = true;
    }

    public boolean startRender() {
        if (out == null) {
            return false;
        }
        
        out.start();
        doesRendering = true;

        return true;
    }

    public void endRender() {
        if (out == null) {
            return;
        }
        
        out.interrupt();
        fileReady = false;
        doesRendering = false;
    }

    public boolean doesRendering() {
        return doesRendering;
    }

    private static void toBytes(byte[] dest, double[] data, int bitRate, boolean isBigEndian) {
        int byteRate = bitRate / 8;

        for (int i = 0; i < data.length; i++) {
            long bits = 0x0000000000000000l;

            switch (byteRate) {
                case 2:
                    bits = Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, Math.round(data[i])));
                    break;
                case 3:
                    bits = Math.max(-0x7FFFFF, Math.min(0x7FFFFF, Math.round(data[i])));
                    break;
                case 4:
                    bits = Float.floatToIntBits((float) data[i]);
                    break;
                case 8:
                    bits = Double.doubleToLongBits(data[i]);
                    break;
                default:
                    bits = Math.max(Byte.MIN_VALUE, Math.min(Byte.MAX_VALUE, Math.round(data[i])));
                    break;
            }

            if (isBigEndian) {
                for (int j = 0; j < byteRate; j++) {
                    dest[byteRate * i + j] = (byte) ((bits >> (8 * (byteRate - j - 1))) & 0xFF);
                }
            } else {
                for (int j = 0; j < byteRate; j++) {
                    dest[byteRate * i + j] = (byte) ((bits >> (8 * j)) & 0xFF);
                }
            }
        }
    }
}
